Hyunjung Im
Frontend Developer
2023-08-01
액션은 호출하는 시점과 횟수에 의존한다. 많은 함수형 프로그래밍 기술과 개념은 코드를 애견과 계산, 데이터로 구분하는 것으로부터 시작한다. 계산: 실행 가능하나 데이터는 그렇지 않다. 데이터: 데이터는 정적이고 보이는 그대로이다. 데이터는 이벤트에 대한 사실이다. 계산은 실행하기 전까지 어떻게 동작할 지 알 수 없다.
JavaScript는 기본적으로 pass by reference 방식을 사용한다. 이것을 방지하기 위해 객체나 배열을 pass by value 형태로 변경하는 방식을 알아야 한다.
copy on write는 세 단계로 되어 있다. 각 단계를 구현하면 copy on write로 동작한다.
function add_element_last(array, elem) {
const new_array = array.slice(); // 1. 복사본 만들기
new_array.push(elem); // 2. 복사본 바꾸기
return new_array; // 3. 복사본 리턴하기
}
shift()
메서드가 좋은 예제이다. 이 메서드는 값을 바꾸는 동시에 배열 첫 번째 항목을 리턴한다. 즉 변경하면서 읽는 동작이다.
function first_element(array) {
return array[0];
}
function drop_first(array) {
array.shift();
} // 인자로 들어온 값을 변경하는 "쓰기"이다. 이 함수를 copy on write로 변경해야 한다.
function drop_first(array) {
const array_copy = array.slice();
array_copy.shift();
return array_copy;
}
// 동작을 감싸기
function shift(array) {
return array.shift();
}
// 읽으면서 쓰기도 하는 함수를 읽기 함수로 바꾸기
function shift(array) {
const array_copy = array.slice();
const first = array.copy.shift();
return {
first: first,
array: array_copy
};
}
// 다른 방법
function shift(array) {
return {
first: first_element(array),
array: drop_first(array)
};
}
일반적으로 불변 데이터 구조는 변경 가능한 데이터 구조보다 메모리를 더 많이 쓰고 느리다. 하지만 불변 데이터 구조를 사용하면서 대용량의 고성능 시스템을 구현하는 사례는 많이 있다.
배열은 slice()
메서드로 복사본을 만들 수 있다. 객체는 Object.assign()
을 이용할 수 있다.
// 원래 코드
function add_item_to_cart(name, price) {
const item = make_cart_item(name, price);
const shopping_cart = add_item(shopping_cart, item);
const total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
black_friday_promotion(shopping_cart); // 이 함수는 인자로 받은 장바구니 값을 바꾼다.
}
// 데이터를 전달하기전에 복사하기
function add_item_to_cart(name, price) {
const item = make_cart_item(name, price);
const shopping_cart = add_item(shopping_cart, item);
const total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
const cart_copy = deepCopy(shopping_cart);
black_friday_promotion(cart_copy); // 넘기기 전에 복사하여 복사한 값을 전달
}
black_friday_promotion()
함수에 결과를 받아야 한다. 복사본을 전달해 black_friday_promotion()
함수가 변경한 cart_copy
가 결괏값이다. 어떻게 해야 cart_copy
를 안전하게 쓸 수 있을까?cart_copy
의 참조를 가진 black_friday_promotion()
함수가 cart_copy
값을 바꾼다면 어떻게 될까? 아마 버그 로 발견될 것이다. 이 문제를 해결하기 위해 방어적 복사 를 적용해야 한다.// 데이터를 전달하기 전후에 복사
function add_item_to_cart(name, price) {
const item = make_cart_item(name, price);
let shopping_cart = add_item(shopping_cart, item);
const total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
const cart_copy = deepCopy(shopping_cart);
black_friday_promotion(cart_copy);
shopping_cart = deepCopy(cart_copy); // 들어오는 데이터를 위한 복사
}
데이터가 안전한 코드에서 나갈 때 복사하기
안전한 코드로 데이터가 들어올 때 복사하기
Copy on write | 방어적 복사 | |
---|---|---|
언제 쓰는가? | 통제할 수 있는 데이터를 바꿀 때 | 신뢰할 수 없는 코드와 데이터를 주고받아야 할 때 |
어디서 쓰는가? | 안전지대 어디서나 | 안전지대의 경계에서 데이터가 오고 갈 때 |
복사 방식 | 얕은 복사 | 깊은 복사 |
계층형 설계는 소프트웨어를 계층으로 구성하는 기술. 각 계층에 있는 함수는 바로 아래 계층에 있는 함수를 이용해 정의한다.
gets_free_shipping() cartTax() // 비즈니스 규칙 (각 계층의 목적)
↓ ↓
remove_item_by_name() calc_total() add_item() setPriceByName() // 장바구니를 위한 동작들
↓ ↓ |
removeItems() add_element_last() / // copy on write
↓ ↓ /
.slice() ↲ // 언어에서 지원하는 배열 관련 기능
직접 구현 패턴을 적용하면 모두 같은 단계의 레벨을 사용해야 한다.
fucntion freeTieClip(cart) {
let hasTie = false;
let hasTieClip = false;
for (let i = 0;i < cart.length; i++) {
const item = cart[i];
if (item.name === "tie") { // 넥타이가 있는지 확인
hasTie = true;
}
if (item.name === "tie_clip") { // 넥타이가 있는지 확인
hasTieClip = true;
}
}
if (hasTie && !hasTieClip) {
const tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip); // 넥타이 클립 추가
}
return cart;
}
freeTieClip()
함수가 알아야 할 필요가 없는 구체적인 내용을 담고 있다.make_item()
함수와 add_item()
함수는 직접 만든 함수이다. 그리고 반복문이나 배열 인덱스 참조 기능은 언어에서 제공하는 기능이다. 직접 만든 함수와 언어 기능은 추상화 수준이 다르다.for (let i = 0; i < cart.length; i++) {
const item = cart[i];
if (item.name === "tie") {
// 넥타이가 있는지 확인
hasTie = true;
}
if (item.name === "tie_clip") {
// 넥타이가 있는지 확인
hasTieClip = true;
}
}
function freeTieClip(cart) {
const hasTie = isInCart(cart, "tie");
const hasTieClip = isInCart(cart, "tie clip");
if (hasTie && !hasTieClip) {
const tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip); // 넥타이 클립 추가
}
return cart;
}
// 반복문을 추출해 새로운 함수 생성
function isInCart(cart, name) {
for (let i = 0; i < cart.length; i++) {
if (cart[i].name === name) {
return true;
}
return false;
}
}
freeTieClip()
이 사용하는 모든 함수는 장바구니가 배열인지 몰라도 된다.추상화 벽은 여러 가지 문제를 해결한다. 그중 하나는 팀 간 책임을 명확하게 나누는 것이다.
작은 인터페이스 패턴은 새로운 코드를 추가할 위치에 관한 것이다. 인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.
앞에서 알아본 패턴 세 개는 계층을 구성하는 것에 관한 패턴이다. 세 개의 패턴은 가장 이상적인 계층 구성을 만드는 방법에 대해 설명하고 있다.
함수는 그래프 위에서 멀어질수록 더 고치기 어렵다.